American Depositary Reciepts (ADRs) are financial instruments that allow US investors to purchase stocks in foreign companies. Argentina has a number of companies listed in US exchanges through ADRs.
In this notebook, we will explore the performance of these ADRs with a focus on the recent months, where political uncertainty sent markets into turmoil: the country’s Merval stock index fell 48% in dollar terms in a single day, the second-largest one-day drop in any of the 94 markets tracked by Bloomberg since 1950[1], causing a 20% devaluation of the Argentine peso and a sharp drop in bond prices.
We will be looking at the end-of-day data of the Argentine ADRs, and their corresponding put and call options.
data_dir = 'data/'
tickers = [
'BMA', 'BFR', 'CEPU', 'CRESY', 'EDN', 'GGAL', 'SUPV', 'IRS',
'IRCP', 'LOMA', 'NTL', 'MELI', 'PAM', 'PZE', 'TEO', 'TS', 'TX',
'TGS', 'YPF'
]
dfs = [pd.read_csv(os.path.join(data_dir, ticker.lower() + '.csv')) for ticker in tickers]
adrs_df = pd.concat(dfs, axis=0, ignore_index=False)
adrs_df.to_csv(os.path.join(data_dir, 'adrs.csv'), index=False)
We begin our exploration of the end-of-day (EOD) data for Argentina's ADRs.
adrs_df = pd.read_csv(os.path.join(data_dir, 'adrs.csv'), index_col='date', parse_dates=['date'])
adrs_df.index = pd.DatetimeIndex(adrs_df.index.date, name='date')
adrs_df.head()
| symbol | close | high | low | open | volume | adjClose | adjHigh | adjLow | adjOpen | adjVolume | divCash | splitFactor | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date | |||||||||||||
| 2006-03-27 | BMA | 23.05 | 23.05 | 22.23 | 22.89 | 1065200 | 15.524521 | 15.524521 | 14.972239 | 15.416759 | 1065200 | 0.0 | 1.0 |
| 2006-03-28 | BMA | 22.38 | 22.47 | 21.90 | 22.47 | 1556100 | 15.073266 | 15.133883 | 14.749979 | 15.133883 | 1556100 | 0.0 | 1.0 |
| 2006-03-29 | BMA | 22.84 | 23.14 | 22.05 | 22.10 | 641300 | 15.383083 | 15.585138 | 14.851006 | 14.884682 | 641300 | 0.0 | 1.0 |
| 2006-03-30 | BMA | 22.75 | 23.10 | 22.70 | 23.00 | 293600 | 15.322467 | 15.558197 | 15.288791 | 15.490846 | 293600 | 0.0 | 1.0 |
| 2006-03-31 | BMA | 22.93 | 22.93 | 22.35 | 22.83 | 113600 | 15.443700 | 15.443700 | 15.053061 | 15.376348 | 113600 | 0.0 | 1.0 |
The data is indexed by date, with the symbol column holding the ticker name, and columns for the open and close prices (the price of the symbol at the start/end of the market day) and the high and low prices seen for the symbol at that date.
We'll begin plotting the adjusted close prices for each symbol (price adjusted for dividends payed and stock splits). The reset of the columns can be safely ingored for our purposes.
Let's zoom in on the 2019 adjusted close prices.
adrs_df.groupby('symbol')['adjClose'].describe()
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| symbol | ||||||||
| BFR | 4936.0 | 8.279068 | 5.706390 | 0.744824 | 3.953318 | 5.724786 | 12.029480 | 25.454491 |
| BMA | 3371.0 | 35.178278 | 25.470378 | 4.999717 | 15.792980 | 24.600919 | 48.906183 | 125.424911 |
| CEPU | 386.0 | 10.988264 | 2.897472 | 3.460000 | 9.060000 | 9.975000 | 12.277500 | 17.919462 |
| CRESY | 4936.0 | 9.937665 | 3.951959 | 2.923092 | 6.744933 | 9.466466 | 12.010526 | 21.638971 |
| EDN | 3099.0 | 15.310731 | 12.471194 | 1.710000 | 6.330000 | 12.110000 | 20.245000 | 62.550000 |
| GGAL | 4795.0 | 13.551952 | 13.195641 | 0.211733 | 5.404357 | 7.966350 | 16.436948 | 71.458310 |
| IRCP | 3971.0 | 15.634198 | 14.424214 | 1.293231 | 4.377059 | 10.018225 | 21.425000 | 62.224671 |
| IRS | 4936.0 | 10.068860 | 5.624760 | 1.904546 | 6.130128 | 8.541985 | 13.279294 | 32.170000 |
| LOMA | 449.0 | 14.069621 | 5.356757 | 5.400000 | 10.340000 | 11.740000 | 21.120000 | 25.020000 |
| MELI | 3025.0 | 138.089838 | 126.898120 | 8.023763 | 59.112264 | 93.592940 | 153.612638 | 690.100000 |
| NTL | 4519.0 | 13.146904 | 8.402332 | 0.392764 | 6.546068 | 12.756651 | 18.681158 | 51.700000 |
| PAM | 2479.0 | 21.467967 | 18.064011 | 2.850000 | 9.885292 | 14.590000 | 30.800000 | 71.650000 |
| PZE | 4608.0 | 5.865744 | 2.463131 | 1.515041 | 4.362068 | 5.365770 | 6.720500 | 14.550000 |
| SUPV | 816.0 | 15.182065 | 7.337212 | 3.160000 | 8.918750 | 13.907507 | 17.840770 | 32.369630 |
| TEO | 4936.0 | 12.194805 | 6.370887 | 0.380288 | 7.642209 | 12.427119 | 16.013961 | 35.963224 |
| TGS | 4936.0 | 4.000758 | 4.341288 | 0.287635 | 1.615702 | 2.457407 | 3.514175 | 20.523048 |
| TS | 4195.0 | 25.943267 | 10.946160 | 2.261653 | 21.353160 | 28.215679 | 34.121053 | 55.724628 |
| TX | 3408.0 | 20.250514 | 6.297409 | 3.256623 | 16.042294 | 19.848379 | 24.567725 | 39.303918 |
| YPF | 4936.0 | 21.578362 | 9.696556 | 3.164889 | 14.031595 | 21.594239 | 29.665596 | 47.309175 |
Next we'll calculate the daily returns.
$$R_n \equiv \frac{S_n - S_{n-1}}{S_{n-1}} \%$$adrs_df['return'] = adrs_df.groupby('symbol')['adjClose'].pct_change() * 100
adrs_df.head()
| symbol | close | high | low | open | volume | adjClose | adjHigh | adjLow | adjOpen | adjVolume | divCash | splitFactor | return | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date | ||||||||||||||
| 2006-03-27 | BMA | 23.05 | 23.05 | 22.23 | 22.89 | 1065200 | 15.524521 | 15.524521 | 14.972239 | 15.416759 | 1065200 | 0.0 | 1.0 | NaN |
| 2006-03-28 | BMA | 22.38 | 22.47 | 21.90 | 22.47 | 1556100 | 15.073266 | 15.133883 | 14.749979 | 15.133883 | 1556100 | 0.0 | 1.0 | -2.906725 |
| 2006-03-29 | BMA | 22.84 | 23.14 | 22.05 | 22.10 | 641300 | 15.383083 | 15.585138 | 14.851006 | 14.884682 | 641300 | 0.0 | 1.0 | 2.055407 |
| 2006-03-30 | BMA | 22.75 | 23.10 | 22.70 | 23.00 | 293600 | 15.322467 | 15.558197 | 15.288791 | 15.490846 | 293600 | 0.0 | 1.0 | -0.394046 |
| 2006-03-31 | BMA | 22.93 | 22.93 | 22.35 | 22.83 | 113600 | 15.443700 | 15.443700 | 15.053061 | 15.376348 | 113600 | 0.0 | 1.0 | 0.791209 |
adrs_df.groupby('symbol')['return'].describe()
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| symbol | ||||||||
| BFR | 4935.0 | 0.053087 | 3.671496 | -55.850622 | -1.734230 | 0.000000 | 1.686544 | 46.760563 |
| BMA | 3370.0 | 0.083384 | 3.282690 | -52.667364 | -1.489327 | 0.000000 | 1.647746 | 27.008149 |
| CEPU | 385.0 | -0.265245 | 4.306204 | -55.915179 | -1.913876 | -0.317460 | 1.581028 | 16.880093 |
| CRESY | 4935.0 | 0.038327 | 2.767597 | -38.090452 | -1.250890 | 0.000000 | 1.180099 | 27.118644 |
| EDN | 3098.0 | 0.051126 | 3.888776 | -58.983957 | -1.690714 | -0.031217 | 1.629187 | 27.551020 |
| GGAL | 4794.0 | 0.094611 | 4.627147 | -56.117370 | -1.626710 | 0.000000 | 1.693722 | 153.623188 |
| IRCP | 3970.0 | 0.142312 | 4.226301 | -32.424983 | -0.681957 | 0.000000 | 0.849814 | 36.986301 |
| IRS | 4935.0 | 0.015710 | 2.675223 | -38.287402 | -1.250000 | 0.000000 | 1.214575 | 18.083573 |
| LOMA | 448.0 | -0.166143 | 4.533969 | -57.298137 | -1.875441 | -0.087351 | 1.505056 | 22.650602 |
| MELI | 3024.0 | 0.165014 | 3.566247 | -21.198668 | -1.400264 | 0.083916 | 1.589953 | 36.000000 |
| NTL | 4518.0 | 0.082807 | 3.342394 | -46.188341 | -1.315789 | 0.000000 | 1.365299 | 30.000000 |
| PAM | 2478.0 | 0.060250 | 2.964323 | -53.815490 | -1.415248 | 0.000000 | 1.411089 | 16.941990 |
| PZE | 4607.0 | 0.066024 | 3.908461 | -19.221411 | -1.457769 | 0.000000 | 1.415805 | 179.843750 |
| SUPV | 815.0 | -0.025345 | 4.183604 | -58.746736 | -1.407739 | 0.000000 | 1.494714 | 28.330206 |
| TEO | 4935.0 | 0.035329 | 3.079372 | -33.375796 | -1.419974 | 0.000000 | 1.450779 | 23.076923 |
| TGS | 4935.0 | 0.077615 | 3.385825 | -48.035488 | -1.494490 | 0.000000 | 1.615534 | 25.203252 |
| TS | 4194.0 | 0.084939 | 2.537497 | -21.309735 | -1.177830 | 0.113344 | 1.351878 | 21.576763 |
| TX | 3407.0 | 0.047116 | 3.028683 | -19.678519 | -1.351580 | 0.036819 | 1.413508 | 49.096099 |
| YPF | 4935.0 | 0.033014 | 2.563941 | -34.052758 | -1.121233 | 0.000000 | 1.123280 | 37.254597 |
Let's plot a histogram of the daily returns for each symbol.
We see most returns cluster around 0, with a few outliers. We can visualize them using a boxenplot.
We can filter the days with 30% or larger movement in prices (either up or down).
Now if we remove outliers, say discard days where return was higher than 10% or lower than -10%:
In finance, it is common to look at the log returns of an asset. Stock prices are assumed to follow a log-normal distribution, hence we should expect log returns to be distributed normally.
$$ln(S_T)\sim N\big[ln(S_0)+(\mu-\frac{\sigma^2}{2})T,\;\sigma^2T\big] \\ ln(\frac{S_T}{S_0})\sim N\big[(\mu-\frac{\sigma^2}{2})T, \;\sigma^2T\big]$$Where $S_T$ is the price of the underlying at time $T$.
For a more detailed discussion on the assumptions of the Black-Scholes-Merton model, see chapter 15 of Options, Futures and Other Derivatives (9th Ed) by John Hull.
You can read more on the distribution of prices and returns here.
adrs_df['log_return'] = np.log(adrs_df['return'] / 100 + 1.)
Now we can calculate the volatility) $\sigma$ for each symbol, defined as the standard deviation of log returns.
As a comparison, we'll add the daily volatility (from 2000 to 2018) for the S&P 500 index, a stock market index that measures the stock performance of the 500 largest publicly traded companies in the United States.
spx_df = pd.read_csv(os.path.join(data_dir, 'spx_2000-2018.csv'),
index_col='date',
parse_dates=['date'])
spx_df['log_return'] = np.log(spx_df['price'] / spx_df['price'].shift(1))
adr_volatility = adrs_df.groupby('symbol')['log_return'].std()
adr_volatility['SPX'] = spx_df['log_return'].std()
sns.barplot(x=adr_volatility.index, y=adr_volatility.values).set_title(
'Daily volatility $\sigma_{daily}$ for each symbol (std of log returns)',
size=16);
Let's plot the mean yearly returns for each symbol. Again, we'll add the mean daily return of \$SPX as a benchmark.
Let's plot the returns that are $3\sigma$ away from the mean. If log returns were truly distributed normally, then we should expect $99.7\%$ of them to lie in the interval $(\mu_R - 3\sigma, \mu_R + 3\sigma)$.
def outlier_filter(symbol_df):
symbol = symbol_df['symbol'].iloc[0]
return symbol_df.loc[(symbol_df['log_return'] -
symbol_df['log_return'].mean()).abs() >= 3 *
adr_volatility[symbol]]
outliers = adrs_df.groupby('symbol').apply(outlier_filter).reset_index(
level=0, drop=True)
sns.scatterplot(x=outliers.index, y='log_return', hue='symbol',
data=outliers).set_title('$3\sigma$ outlier daily returns',
size=16);
We see a large number of outlier return days. To put that in perspective, let's calculate the proportion of outlier returns for each symbol in the data, that is the number of days where $3\sigma$ returns where observed over the total number of observations.
Finally, we'll look at the cumulative log returns over time.
pivoted = adrs_df.pivot(columns='symbol', values='log_return')
pivoted.cumsum().apply(np.exp).plot(title='Cumulative log returns');
Now we'll examine the options end-of-day data for the ADRs. Options are derivative contracts based on an underlying asset such as stocks. They offer the buyer the opportunity to buy or sell the underlying asset at a given price (strike price). You can find more information on options in this notebook.
| underlying | underlying_last | exchange | optionroot | type | expiration | strike | last | net | bid | ask | volume | openinterest | impliedvol | delta | gamma | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quotedate | ||||||||||||||||
| 2019-07-03 | TEO | 17.79 | CBOE | TEO190719C00002500 | call | 2019-07-19 | 2.5 | 0.0 | 0.0 | 13.0 | 17.8 | 0 | 0 | 0.0200 | 1.0000 | 0.0000 |
| 2019-07-03 | TEO | 17.79 | CBOE | TEO190719C00005000 | call | 2019-07-19 | 5.0 | 0.0 | 0.0 | 10.5 | 15.2 | 0 | 0 | 6.3229 | 0.9446 | 0.0045 |
| 2019-07-03 | TEO | 17.79 | CBOE | TEO190719C00007500 | call | 2019-07-19 | 7.5 | 0.0 | 0.0 | 8.0 | 12.8 | 0 | 0 | 3.5717 | 0.9336 | 0.0093 |
| 2019-07-03 | TEO | 17.79 | CBOE | TEO190719C00010000 | call | 2019-07-19 | 10.0 | 0.0 | 0.0 | 5.5 | 10.2 | 0 | 0 | 2.0354 | 0.9375 | 0.0156 |
| 2019-07-03 | TEO | 17.79 | CBOE | TEO190719C00012500 | call | 2019-07-19 | 12.5 | 0.0 | 0.0 | 3.0 | 7.8 | 0 | 199 | 1.2856 | 0.9215 | 0.0297 |
The options data is also indexed by date. These are the most important columns:
underlying: The ticker of the underlying asset.underlying_last: The last quoted price of the underlying asset.optionroot: The name of the contract.type: Contract type (put or call)strike: The price at which owner can execute (buy/sell underlying).expiration: Date of expiration of the option.bid: The price at which investor can sell this contract.ask: The price at which investor can buy this contract.openinterest: The total number of contract outstanding.impliedvol: Volatility of the underlying as implied by the option price (according to BSM model)Let's plot the volatility smile for each symbol at 2019-08-09.
The volatility smile plots the implied volatility (IV, a measure of the volatility of an underlying security as implied by the option prices) at the different strike levels.
We'll plot the IV for puts and calls for each symbol. The dashed line represents the spot price.
Now let's try the same plot for the following Monday (2019-08-12). That day, the MERVAL (an index that tracks the biggest companies listed in the Buenos Aires Stock Exchange) crashed and lost close to 50% of its USD value.
Next, we'll analyse how option prices changed during the month of August, 2019. Let's find the 10 most actively traded options (those with the highest open interest) for each symbol at the start of the month.
august_options = adr_options.loc['2019-08']
def filter_active(symbol_df, option_type='call'):
return symbol_df.loc[symbol_df['type'] == option_type].nlargest(
n=10, columns='openinterest')
month_start_date = august_options.index.min()
first_trading_day = august_options.loc[month_start_date]
most_active_calls = first_trading_day.groupby('underlying').apply(
filter_active).reset_index(level=0, drop=True)
most_active_puts = first_trading_day.groupby('underlying').apply(
lambda df: filter_active(df, 'put')).reset_index(level=0, drop=True)
call_contracts = most_active_calls['optionroot']
put_contracts = most_active_puts['optionroot']
august_active_calls = august_options.loc[august_options['optionroot'].isin(call_contracts)]
august_active_puts = august_options.loc[august_options['optionroot'].isin(put_contracts)]
august_active_calls['day'] = august_active_calls.index.strftime('%d')
august_active_puts['day'] = august_active_puts.index.strftime('%d')
We'll plot the evolution of the ask price for the actively traded calls through August 2019.
As a comparison, let's try plotting the option prices for June 2019.
june_options = adr_options.loc['2019-06']
october_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_October_2015.csv'),
index_col='quotedate',
parse_dates=['quotedate', 'expiration'])
november_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_november_2015.csv'),
index_col='quotedate',
parse_dates=['quotedate', 'expiration'])
december_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_December_2015.csv'),
index_col='quotedate',
parse_dates=['quotedate', 'expiration'])
We'll examine the options data for January 2008, the beginning of the mortgage crisis. We only have data for 3 companies: \$MELI, \\$TS and \$TX
january_2008_options = pd.read_csv(os.path.join(data_dir, 'adr_options_january_2008.csv'),
index_col='quotedate',
parse_dates=['quotedate', 'expiration'])
Countries such as Argentina exhibit high political beta: stocks show extreme sensitivity to political events. The MERVAL index nearly quadrupled its value in Pesos since December 2015, only to drop by %40 in a single day in August 2019.
Investors tend to exacerbate bull runs, discounting future growth in the current stock prices. At the prospect of regulatory changes and political turnover, they panic sell and seek less risky assets. There remains to be seen whether savvy traders can exploit both the overconfident bulls and the panicking bears, buying volatility from the former to sell it at a profit to the latter.